package com.platform.boot.config;

import lombok.extern.log4j.Log4j2;
import org.springframework.boot.autoconfigure.security.reactive.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestAttributeHandler;
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * SecurityConfiguration on com.platform.gateway.config
 *
 * @author <a href="https://github.com/vnobo">Alex bob</a>
 */
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.authorizeExchange(exchange -> {
                    exchange.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
                    exchange.anyExchange().authenticated();
                })
                .formLogin(Customizer.withDefaults())
                .httpBasic(httpBasicSpec -> httpBasicSpec
                        .authenticationEntryPoint(new CustomServerAuthenticationEntryPoint()))
                .securityContextRepository(new WebSessionServerSecurityContextRepository())
                .csrf(csrf -> csrf.requireCsrfProtectionMatcher(new IgnoreRequireCsrfProtectionMatcher())
                        .csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
                        .csrfTokenRequestHandler(new ServerCsrfTokenRequestAttributeHandler()));
        return http.build();
    }

    static class IgnoreRequireCsrfProtectionMatcher implements ServerWebExchangeMatcher {
        private final Set<HttpMethod> allowedMethods = new HashSet<>(
                Arrays.asList(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE, HttpMethod.OPTIONS));
        private final List<ServerWebExchangeMatcher> allowedMatchers = List.of(
                new PathPatternParserServerWebExchangeMatcher("/oauth2/v1/none", HttpMethod.POST)
        );

        @Override
        public Mono<MatchResult> matches(ServerWebExchange exchange) {
            Mono<ServerWebExchangeMatcher.MatchResult> ignoreMono = new OrServerWebExchangeMatcher(allowedMatchers)
                    .matches(exchange).filter(ServerWebExchangeMatcher.MatchResult::isMatch)
                    .flatMap(res -> MatchResult.notMatch())
                    .switchIfEmpty(MatchResult.match());

            return Mono.just(exchange.getRequest()).flatMap((r) -> Mono.justOrEmpty(r.getMethod()))
                    .filter(allowedMethods::contains).flatMap((m) -> MatchResult.notMatch())
                    .switchIfEmpty(ignoreMono);
        }
    }

    @Log4j2
    static class CustomServerAuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint {

        @Override
        public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
            String xRequestedWith = "X-Requested-With";
            String xmlHttpRequest = "XMLHttpRequest";
            List<String> requestedWith = exchange.getRequest().getHeaders().get(xRequestedWith);
            log.error("认证失败! 错误信息: {}", e.getMessage());
            if (log.isDebugEnabled()) {
                e.printStackTrace();
            }
            if (requestedWith != null && requestedWith.contains(xmlHttpRequest)) {
                return Mono.defer(() -> Mono.just(exchange.getResponse())).flatMap((response) -> {
                    response.setStatusCode(HttpStatus.UNAUTHORIZED);
                    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                    String body = "{\"code\":401,\"msg\":\"认证失败,检查你的用户名,密码是否正确或安全密钥是否过期!\",\"errors\":\""
                            + e.getMessage() + "\"}";
                    DataBufferFactory dataBufferFactory = response.bufferFactory();
                    DataBuffer buffer = dataBufferFactory.wrap(body.getBytes(Charset.defaultCharset()));
                    return response.writeWith(Mono.just(buffer))
                            .doOnError((error) -> DataBufferUtils.release(buffer));
                });
            }
            return super.commence(exchange, e);
        }
    }
}